/**
 * @author David Young
 */
package csab.core;

import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

import org.newdawn.slick.Graphics;

import csab.core.utility.Observer;
import csab.core.utility.ObserverMessage;

/**
 * Represents a game object within the engine. Abstract and must be
 * over-ridden.
 * @author David Young
 */
public abstract class Entity {
	/** 
	 * Intended to be an ObserverMessage String
	 * that indicates that this object has been
	 * destroyed.
	 * @see csab.core.utility.ObserverMessage
	 */
	public static String DESTROYED_MSG = "destroyed";
	private static int currentID;
	private static int MAX_XRANGE, MIN_XRANGE, MAX_YRANGE, MIN_YRANGE;
	protected final static int MAX_XSPEED = 10000;
	protected final static int MAX_YSPEED = 10000;
	
	private boolean atMapEdge;
	private int id;
	private boolean isDestroyed;
	private boolean isDrawable;
	private String name;
	private List<Observer> observers;
	private float vx, vy;
	private int width, height;
	private float x, y, lastx, lasty;
	
	static {
		currentID = 0;
		setWorldXRange(0, Integer.MAX_VALUE);
		setWorldYRange(0, Integer.MAX_VALUE);
	}
	
	/**
	 * Get whether or not this object is at the edge of
	 * the map.
	 * @return true if the object is at the edge of the
	 * map.
	 */
	public boolean isAtMapEdge()
	{
		return atMapEdge;
	}
	
	/** 
	 * Gets the number of entities that have been spawned by
	 * the game engine. Does not necessarily represent the 
	 * number of entities still in memory however.
	 * @return The number of entities that have been created. 
	 */
	public static int getEntityCount()
	{
		return currentID;
	}
	
	public static int getWorldXRangeMax()
	{
		return MAX_XRANGE;
	}
	
	public static int getWorldXRangeMin()
	{
		return MIN_XRANGE;
	}
	
	public static int getWorldYRangeMax()
	{
		return MAX_YRANGE;
	}
	public static int getWorldYRangeMin()
	{
		return MIN_YRANGE;
	}
	
	/**
	 * Set the maximum x-axis range to the appropriate bounds.
	 * @param min Lowest x-axis position an entity can have.
	 * @param max Highest x-axis position and entity can have.
	 */
	public static void setWorldXRange(int min, int max)
	{
		MAX_XRANGE = max;
		MIN_XRANGE = min;
	}

	/**
	 * Set the maximum y-axis range to the appropriate bounds.
	 * @param min Lowest y-axis position an entity can have.
	 * @param max Highest y-axis position and entity can have.
	 */
	public static void setWorldYRange(int min, int max)
	{
		MAX_YRANGE = max;
		MIN_YRANGE = min;
	}
	
	/**
	 * Constructor for an object of this class.
	 * @param name The name which will be assigned to
	 * this entity. Does not have to be unique.
	 */
	public Entity(String name)
	{
		this.name = name;
		id = currentID;
		x = 0;
		y = 0;
		vx = 0;
		vy = 0;
		width = 0;
		atMapEdge = false;
		height = 0;
		lastx = 0;
		lasty = 0;
		isDrawable = true;
		currentID++;
		observers = new LinkedList<Observer>();
		isDestroyed = false;
	}
	
	/**
	 * Add an Observer to be notified when this
	 * object is destroyed.
	 * @param obs The Observer to add.
	 * @see csab.core.utility.Observer
	 */
	public void addObserver(Observer obs)
	{
		observers.add(obs);
	}
	
	/**
	 * Destroy this object (free up resources).
	 * Do not use this Entity after calling this
	 * method.
	 */
	public void doDestroy()
	{
		this.setDrawable(false);
		notifyObservers();
		destroy();
	}
	
	/**
	 * Perform a movement operation on this object utilizing
	 * velocity and acceleration.
	 * @param delta The time passed since the last game tick (update).
	 * @param xaccel The acceleration to apply to the x-axis velocity.
	 * @param yaccel The acceleration to apply to the y-axis velocity.
	 */
	public void doMove(int delta, float xaccel, float yaccel)
	{
		float nxacc = (xaccel / 1000) * delta;
		float nyacc = (yaccel / 1000) * delta;
		float newvx = nxacc + this.vx;
		float newvy = nyacc + this.vy; 
		if(newvx > MAX_XSPEED || newvx < MAX_XSPEED * -1)
		{
			newvx = MAX_XSPEED;
		}
		
		if(newvy > MAX_YSPEED || newvy < MAX_YSPEED * -1)
		{
			newvy = MAX_YSPEED;
		}
		
		this.vx = newvx;
		this.vy = newvy;
			
		float newx = ((getVX() * ((float)delta / 1000)) + getX());
		float newy = ((getVY() * ((float)delta / 1000)) + getY());
		this.setPos(newx, newy);
	}
	
	/**
	 * Draw this entity to the specified (x,y) position.
	 * @param x X-axis position to draw to.
	 * @param y Y-axis position to draw to.
	 */
	public abstract void draw(int x, int y, Graphics g);
	
	/* (non-Javadoc)
	 * @see java.lang.Object#equals(java.lang.Object)
	 */
	public final boolean equals(Object o)
	{
		Collidable obj = (Collidable)o;
		return obj != null && obj.getID() == this.getID(); 
	}
	
	/**
	 * Get the drawing height of this object.
	 * @return The height of this object.
	 */
	public int getHeight() {
		return height;
	}
	
	
	/**
	 * Return the entity ID of this object. This is guaranteed to be
	 * unique.
	 * @return The entity ID of this object.
	 */
	public int getID()
	{
		return id;
	}
	
	/**
	 * Get the last y-axis position of this object.
	 * @return The previous x-axis position of this object.
	 */
	public float getLastX() { return lastx; }
	
	/**
	 * Get the last y-axis position of this object.
	 * @return The previous y-axis position of this object.
	 */
	public float getLastY() { return lasty; }
	
	/**
	 * Get the name of this Entity.
	 * @return The name of this Entity.
	 */
	public String getName() {
		return name;
	}
	
	/**
	 * Get the x-axis velocity of this object.
	 * @return The x-axis velocity of this object.
	 */
	public float getVX() { return vx; }
	
	/**
	 * Get the y-axis velocity of this object.
	 * @return The y-axis velocity of this object.
	 */
	public float getVY() { return vy; }
	
	/**
	 * Get the drawing width of this object.
	 * @return The width of this object.
	 */
	public int getWidth() {
		return width;
	}
	
	/**
	 * Get the x-axis position of this object.
	 * @return The x-axis position of this object.
	 */
	public float getX() { return x; }
	
	/**
	 * Get the y-axis position of this object.
	 * @return The y-axis position of this object.
	 */
	public float getY() { return y; }
	
	/* (non-Javadoc)
	 * @see java.lang.Object#hashCode()
	 */
	public int hashCode()
	{
		return getID();
	}
	
	/**
	 * Check to see if this entity has moved from its prior/current
	 * position.
	 * @return true if the object has moved, false if not.
	 */
	public final boolean hasMoved()
	{
		return getLastX() != getX() || getLastY() != getY();
	}
	
	/**
	 * Get whether or not this Entity is destroyed.
	 * @return true if this Entity is destroyed.
	 */
	public boolean isDestroyed()
	{
		return isDestroyed;
	}

	/**
	 * Get whether or not this object should be drawn to the screen.
	 * @return true if this object should be drawn.
	 */
	public boolean isDrawable() {
		return isDrawable;
	}
	
	/**
	 * Inform all currently assigned Observers that
	 * this object has been destroyed.
	 * @see csab.core.utility.Observer
	 */
	public void notifyObservers()
	{
		Iterator<Observer> itr = observers.iterator();
		ObserverMessage msg = new ObserverMessage(DESTROYED_MSG);
		while(itr.hasNext())
		{
			itr.next().handleObservation(msg);
		}
	}
	
	/**
	 * Set whether or not this object should be drawn to the screen.
	 * @param isDrawable Determine whether or not this object is meant
	 * to be drawn to the screen.
	 */
	public void setDrawable(boolean isDrawable) {
		this.isDrawable = isDrawable;
	}

	/**
	 * Set the new drawing height of this object.
	 * @param height The new drawing height of this object.
	 */
	public void setHeight(int height) {
		this.height = height;
	}

	/**
	 * Set the last x-axis position of this object.
	 * @param lastx The new x-axis value for the internal
	 * lastX variable of this object. This value is meant
	 * and used to represent the prior x-axis position to 
	 * the current x-axis position.
	 */
	public void setLastX(float lastx) {
		this.lastx = lastx;
	}

	/**
	 * Set the last y-axis position of this object.
	 * @param lasty The new y-axis value for the internal
	 * lastY variable of this object. This value is meant
	 * and used to represent the prior y-axis position to 
	 * the current y-axis position.
	 */
	public void setLastY(float lasty) {
		this.lasty = lasty;
	}

	/**
	 * Set the name of this Entity.
	 * @param name The new name of this Entity.
	 */
	public void setName(String name)
	{
		this.name = name;
	}

	/**
	 * Update the position coordinates of this object.
	 * @param x The new x-axis location of this object.
	 * @param y The new y-axis location of this object.
	 */
	public void setPos(float x, float y) {
		setX(x);
		setY(y);
	}

	/**
	 * Update the velocity coordinates of this object.
	 * @param vx The new x-axis velocity of this object.
	 * @param vy The new y-axis velocity of this object.
	 */
	public void setVel(float vx, float vy)
	{
		setVX(vx);
		setVY(vy);
	}

	/**
	 * Set the x-axis velocity of this object.
	 * @param vx The new x-axis velocity of this object.
	 */
	public void setVX(float vx) { this.vx = vx; }

	/**
	 * Set the y-axis velocity of this object.
	 * @param vy The new y-axis velocity of this object.
	 */
	public void setVY(float vy) { this.vy = vy; }
	
	/**
	 * Set the drawing width of this Entity.
	 * @param width The new width of this object.
	 */
	public void setWidth(int width) {
		this.width = width;
	}
	
	/**
	 * Set the x-axis position of this object.
	 * @param x The new x-axis position of this object.
	 */
	public void setX(float x) { 
		this.lastx = this.x;
		if(x < MIN_XRANGE)
		{
			atMapEdge = true;
			this.x = MIN_XRANGE;
		}
		else if(x + this.width > MAX_XRANGE)
		{
			atMapEdge = true;
			this.x = MAX_XRANGE - this.width;
		}
		else
		{
			atMapEdge = false;
			this.x = x; 
		}

	}
	
	/**
	 * Set the y-axis position of this object.
	 * @param y The new y-axis position of this object.
	 */
	public void setY(float y) { 
		this.lasty = this.y;
		if(y < MIN_YRANGE)
		{
			this.y = MIN_YRANGE;
			this.setVY(0);
		}
		else if(y + this.height > MAX_YRANGE)
			this.y = MAX_YRANGE - this.height;
		else	
			this.y = y; 
	}
	
	/* (non-Javadoc)
	 * @see java.lang.Object#toString()
	 */
	public String toString()
	{
		return "id(" + id +") " + "name(" + name + ")";
	}
	
	/* (non-Javadoc)
	 * Must be implemented to free resources
	 * that are allocated to this Entity.
	 */
	protected abstract void destroy();
}
